// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <ucommon/secure.h>
#include <ctype.h>
#include <sys/stat.h>

using namespace UCOMMON_NAMESPACE;

class entry : public NamedObject
{
public:
    inline entry(NamedObject **root, char *id) :
        NamedObject(root, id, 1) {};

    const char *name;

    fsys::fileinfo_t inode;
};

static shell::flagopt helpflag('h',"--help",    _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::flagopt all('a', "--all", _TEXT("show all files"));
static shell::flagopt ignore('i', "--ignore", _TEXT("ignore errors"));
static shell::flagopt longform('l', "--long", _TEXT("long list format"));
static shell::flagopt reverse('r', "--reverse", _TEXT("reverse sort order"));
static shell::flagopt timesort('t', "--time", _TEXT("sort by time"));

static const char *argv0 = "ls";
static unsigned exit_code = 0;
static unsigned widest = 0;
static memalloc mpager(4096);
static bool dumped = false;
static NamedObject *files = NULL;
static unsigned width = 80;

static void report(const char *path, int code)
{
    const char *err = _TEXT("i/o error");

    switch(code) {
    case EACCES:
    case EPERM:
        err = _TEXT("permission denied");
        break;
    case EROFS:
        err = _TEXT("read-only file system");
        break;
    case ENODEV:
    case ENOENT:
        err = _TEXT("no such file or directory");
        break;
    case ENOTDIR:
        err = _TEXT("not a directory");
        break;
    case ENOTEMPTY:
        err = _TEXT("directory not empty");
        break;
    case ENOSPC:
        err = _TEXT("no space left on device");
        break;
    case EBADF:
    case ENAMETOOLONG:
        err = _TEXT("bad file path");
        break;
    case EBUSY:
    case EINPROGRESS:
        err = _TEXT("file or directory busy");
        break;
    case EINTR:
        err = _TEXT("operation interupted");
        break;
#ifdef  ELOOP
    case ELOOP:
        err = _TEXT("too many sym links");
        break;
#endif
    }

    if(is(ignore))
        fprintf(stderr, "%s: %s: %s\n", argv0, path, err);
    else
        shell::errexit(1, "*** %s: %s: %s\n", argv0, path, err);

    exit_code = 1;
}

static void add(const char *fpath, const char *prefix = ".")
{
    string_t fn = str(prefix) + "/" + str(fpath);
    char buf[512];
    fsys::fileinfo_t inode;
    int err;
    static unsigned long uniq = 0;

    if(fsys::is_hidden(*fn) && !is(all))
        return;

    err = fsys::info(*fn, &inode);
    if(err) {
        report(fpath, err);
        memset(&inode, 0, sizeof(inode));
    }

    size_t size = strlen(fpath);
    caddr_t mp = (caddr_t)mpager.zalloc(sizeof(entry));
    char *path = mpager.dup(fpath);
    char *id = path;

    const char *fname = strrchr(fpath, '/');
    if(!fname)
        fname = strrchr(fpath, '\\');

    if(fname)
        ++fname;
    else
        fname = path;

    if(is(timesort)) {
        struct tm *dt = localtime(&inode.st_mtime);
        snprintf(buf, sizeof(buf), "%04d%02d%02d %02d%02d%02d%s %08ld",
            dt->tm_year, dt->tm_mon, dt->tm_mday, dt->tm_hour, dt->tm_min, dt->tm_sec, fname, ++uniq);
        id = mpager.dup(buf);
    }

    entry *fp = new(mp) entry(&files, id);
    fp->name = path;
    fp->inode = inode;
    if(fsys::is_dir(&inode))
        ++size;
    if(size > widest)
        widest = size;
}

static void dump(const char *path)
{
    unsigned count = NamedObject::count(&files, 1);
    unsigned cols = 1, col = 0, rows, row, offset, next;
    const char *months[] = {
        _TEXT("Jan"),
        _TEXT("Feb"),
        _TEXT("Mar"),
        _TEXT("Apr"),
        _TEXT("May"),
        _TEXT("Jun"),
        _TEXT("Jul"),
        _TEXT("Aug"),
        _TEXT("Sep"),
        _TEXT("Oct"),
        _TEXT("Nov"),
        _TEXT("Dec")
    };

    if(!count)
        return;

    if(dumped)
        printf("\n%s:\n", path);

    dumped = true;

    if(widest > (width / 2) - 2)
        cols = 1;
    else
        cols = (width / (widest + 1));

    if(!fsys::is_tty(shell::output()) || is(longform))
        cols = 1;

    NamedObject **index = NamedObject::index(&files, 1);
    index = NamedObject::sort(index, count);
    if((is(reverse) && !is(timesort)) || (is(timesort) && !is(reverse))) {
        unsigned first = 0;
        unsigned last = count - 1;
        while(first < last) {
            swap<NamedObject *>(index[first], index[last]);
            ++first;
            --last;
        }
    }

    rows = ((count + (cols - 1)) / cols);
    for(row = 0; row < rows; ++row) {
        for(col = 0; col < cols; ++col) {
            offset = col * rows + row;
            next = (col + 1) * rows + row;
            if(offset >= count)
                continue;

            entry *node = (entry *)index[offset];
            const char *fn = node->name;
            size_t len = strlen(fn);
            struct tm *dt;

            dt = localtime(&node->inode.st_mtime);

            if(is(longform)) {
                printf("%10ld %s %02d %02d:%02d %s",
                    node->inode.st_size,
                    months[dt->tm_mon], dt->tm_mday,
                    dt->tm_hour, dt->tm_min, fn);
            }
            else
                printf("%s", fn);

            if(fsys::is_dir(&node->inode)) {
                fputc('/', stdout);
                ++len;
            }

            while(next < count && len++ < widest + 1)
                fputc(' ', stdout);
        }
        fputc('\n', stdout);
    }

    delete[] index;
    mpager.purge();
    widest = 0;
    files = NULL;
}

static void list(const char *path)
{
    char filename[256];
    dir_t dir;

    dir.open(path);
    if(!is(dir))
        report(path, dir.err());

    while(is(dir) && dir.read(filename, sizeof(filename)) > 0)
        add(filename, path);
    dump(path);
}

PROGRAM_MAIN(argc, argv)
{
    shell::bind("ls");
    shell args(argc, argv);
    argv0 = args.argv0();
    unsigned count = 0;

    argv0 = args.argv0();

    if(is(helpflag) || is(althelp)) {
        printf("%s: %s %s\n", _TEXT("Usage"), argv0, _TEXT("[options] [paths]..."));
        printf("%s\n\n", _TEXT("List files from directories"));
        printf("%s\n", _TEXT("Options:"));
        shell::help();
        printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
        PROGRAM_EXIT(0);
    }

    if(!args())
        list(".");

    if(args() == 1) {
        if(fsys::is_dir(args[0]))
            list(args[0]);
        else {
            add(args[0]);
            dump(".");
        }
    }
    else {
        for(count = 0; count < args(); ++count) {
            if(fsys::is_dir(args[count]))
                continue;
            add(args[count]);
        }

        if(widest)
            dump(".");

        for(count = 0; count < args(); ++count) {
            if(fsys::is_dir(args[count])) {
                if(!dumped)
                    printf("%s:\n", args[count]);
                list(args[count]);
            }
        }
    }

    PROGRAM_EXIT(exit_code);
}

